﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Media.Media3D;
using System.Windows.Media.Effects;
using System.Windows.Interop;
using System.Windows.Media.Composition;
using System.Windows.Threading;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private Point3DCollection pointCollection = new Point3DCollection();
        private Vector3D[] normals;
        private Vector3D vPrevMousePosition;
        private Vector3D vCurrMousePosition;
        private ModelVisual3D cube = new ModelVisual3D();
        private DispatcherTimer updateStatsTimer = new DispatcherTimer();
        private PerspectiveCamera cam;
        private const double RotationCoefficient = 0.001;

        public Window1()
        {
            InitializeComponent();
            DrawSquare();
            CalcNormals();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            view.Height = this.ActualHeight;
            view.Width = this.ActualWidth;
            this.KeyDown += new KeyEventHandler(Window1_KeyDown);
            this.SizeChanged += new SizeChangedEventHandler(Window1_SizeChanged);
            this.MouseMove += new MouseEventHandler(Window1_MouseMove);
            this.MouseDown += new MouseButtonEventHandler(Window1_MouseDown);

            cam = (PerspectiveCamera)view.Camera;
            updateStatsTimer.Interval = new TimeSpan(0, 0, 0, 0, 100);
            updateStatsTimer.Tick += delegate(object o, EventArgs ea)
            {
                Matrix3D matrix = GetCameraMatrix();
                lblMousePos.Content = "Camera Matrix: \r\n";
                lblMousePos.Content += "M11:" + matrix.M11 + " M12:" + matrix.M12 + " M13:" + matrix.M13 + " M14:" + matrix.M14 + "\r\n";
                lblMousePos.Content += "M21:" + matrix.M21 + " M22:" + matrix.M22 + " M23:" + matrix.M23 + " M24:" + matrix.M24 + "\r\n";
                lblMousePos.Content += "M31:" + matrix.M31 + " M32:" + matrix.M32 + " M33:" + matrix.M33 + " M34:" + matrix.M34 + "\r\n";
                lblMousePos.Content += "M44:" + matrix.M44;
                lblCamPosition.Content = "Cam X:" + cam.Transform.Value.OffsetX + " Y:" + cam.Transform.Value.OffsetY + " Z:" + cam.Transform.Value.OffsetZ;
            };
            updateStatsTimer.Start();

        }

        void Window1_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                Point mousePos = new Point();
                mousePos = e.GetPosition(view);

                // Vector of the pick ray
                Vector3D start = new Vector3D();
                start.X = ((2.0f * mousePos.X) / (view.ActualWidth) - 1) / cam.Transform.Value.M11;
                start.Y = ((2.0f * mousePos.Y) / (view.ActualHeight) - 1) / cam.Transform.Value.M22;
                start.Z = 1.0;

                Vector3D end = new Vector3D();

                // Get view matrix from camera
                Matrix3D matView = new Matrix3D();
                
                // Get world matrix from camera
                Matrix3D matWorld = new Matrix3D();

                // Calc world * view
                Matrix3D worldView = new Matrix3D();


                worldView = matWorld * matView;
                // Take inverse of world matrix
                worldView.Invert();

                

                // Transform pick ray to 3d space
                Vector3D pickRay = new Vector3D();
                Vector3D origin = new Vector3D();
                pickRay.X = start.X * cam.Transform.Value.M11 * start.Y * cam.Transform.Value.M21 * start.Z * cam.Transform.Value.M31;
                pickRay.Y = start.X * cam.Transform.Value.M12 * start.Y * cam.Transform.Value.M22 * start.Z * cam.Transform.Value.M33;
                pickRay.Z = start.X * cam.Transform.Value.M13 * start.Y * cam.Transform.Value.M23 * start.Z * cam.Transform.Value.M33;
                origin.X = cam.Transform.Value.OffsetX;
                origin.Y = cam.Transform.Value.OffsetY;
                origin.Z = cam.Transform.Value.OffsetZ;

                Point3D p1, p2, p3;
                p1 = new Point3D(start.X, start.Y, start.Z);
                p2 = new Point3D(end.X, end.Y, end.Z);
                p3 = new Point3D(start.X - 0.5, start.Y, start.Z);

                Point3DCollection positions = new Point3DCollection() { p1, p2, p3 };
                Vector3D norm = normal(p1, p2, p3);

                DiffuseMaterial mat = new DiffuseMaterial(new SolidColorBrush(Colors.Red));
                MeshGeometry3D mesh = new MeshGeometry3D();

                mesh.Positions = positions;
                mesh.Normals = new Vector3DCollection() { norm };
                mesh.TextureCoordinates = new PointCollection(){new Point(0,0),
                                                                new Point(1,0),
                                                                new Point(0,1)};

                GeometryModel3D geom = new GeometryModel3D(mesh, mat);
                geom.BackMaterial = mat;

                ModelVisual3D model = new ModelVisual3D();
                model.Content = geom;

                view.Children.Add(model);
                double result = intersect(start, end, cam.LookDirection, normals[0]);
                lblIntersect.Content = "Intersect: " + result.ToString();
            }
            else if (e.RightButton == MouseButtonState.Pressed)
            {
                vPrevMousePosition = TransformPoint(e.GetPosition(view));
            }
        }

        void Window1_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            view.Height = this.ActualHeight;
            view.Width = this.ActualWidth;
        }

        void Window1_KeyDown(object sender, KeyEventArgs e)
        {
            Matrix3D matrix = GetCameraMatrix();

            if (e.Key == Key.D || e.Key == Key.Right)
            {
                matrix.OffsetX += 0.1;
            }
            if (e.Key == Key.A || e.Key == Key.Left)
            {
                matrix.OffsetX -= 0.1;
            }

            if (e.Key == Key.W || e.Key == Key.Up)
            {
                matrix.OffsetZ -= 0.1;
            }

            if (e.Key == Key.S || e.Key == Key.Down)
            {
                matrix.OffsetZ += 0.1;
            }

            if (e.Key == Key.Subtract || e.Key == Key.LeftShift)
            {
                matrix.OffsetY += 0.1;
            }

            if (e.Key == Key.Add || e.Key == Key.Space)
            {
                matrix.OffsetY -= 0.1;
            }

            view.Camera.Transform = new MatrixTransform3D(matrix);
            view.InvalidateVisual();
        }

        void Window1_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.RightButton == MouseButtonState.Pressed)
            {
                vCurrMousePosition = TransformPoint(Mouse.GetPosition(view));
                Vector3D cameraDirection = cam.LookDirection;
                Vector3D mouseMovement = vCurrMousePosition - vPrevMousePosition;
                if (mouseMovement.Length == 0)
                    return;

                Vector3D rotationAxis = Vector3D.CrossProduct(mouseMovement, cameraDirection);
                double rotationAngle = RotationCoefficient * mouseMovement.Length;
                MatrixTransform3D transform = cam.Transform as MatrixTransform3D;
                Quaternion rotation = new Quaternion(rotationAxis, rotationAngle * 180 / Math.PI);
                Matrix3D matrix = transform.Matrix;
                matrix.Rotate(rotation);
                cam.Transform = new MatrixTransform3D(matrix);
                
                view.InvalidateVisual();
            }
        }


        private void RayCast()
        {

        }

        private Vector3D TransformPoint(Point point)
        {
            PerspectiveCamera cam = (PerspectiveCamera)view.Camera;
            Vector3D j = -cam.UpDirection;
            Vector3D k = cam.LookDirection;
            Vector3D i = -Vector3D.CrossProduct(j, k);
            i.Normalize();
            j.Normalize();
            k.Normalize();

            // point is the x and y mouse position
            double x = point.X - view.ActualWidth / 2;
            double y = point.Y - view.ActualHeight / 2;
            double z = 0;
            return x * i + y * j + z * k;
        }

        private void DrawSquare()
        {
            MeshGeometry3D mesh = new MeshGeometry3D();
            AmbientLight light = new AmbientLight(Colors.LightBlue);
            SolidColorBrush brush = new SolidColorBrush(Colors.Red);
            LinearGradientBrush lgbrush = new LinearGradientBrush(Colors.Gray, Colors.Black, new Point(0.0, 0.0), new Point(1.0, 1.0));

            SpecularMaterial specmat = new SpecularMaterial(new SolidColorBrush(Colors.Black), 90.0);
            specmat.Color = Colors.Black;

            DiffuseMaterial mat = new DiffuseMaterial();
            mat.AmbientColor = Colors.White;
            mat.Color = Colors.Red;
            mat.Brush = lgbrush;
            pointCollection = new Point3DCollection() { new Point3D(-1, -1, -1),
                                                        new Point3D( 1, -1, -1),
                                                        new Point3D( 1,  1, -1),

                                                        new Point3D(-1,  1,- 1),            
                                                        new Point3D(-1, -1, -1),
                                                        new Point3D(-1,  1, -1),

                                                        new Point3D(-1,  1,  1),
                                                        new Point3D(-1, -1,  1),
                                                        new Point3D( 1, -1, -1),

                                                        new Point3D(-1, -1, -1),
                                                        new Point3D(-1, -1,  1),
                                                        new Point3D( 1, -1,  1),

                                                        new Point3D( 1,  1, -1),
                                                        new Point3D( 1, -1, -1),
                                                        new Point3D( 1, -1,  1),

                                                        new Point3D( 1,  1,  1),
                                                        new Point3D( 1, -1,  1),
                                                        new Point3D(-1, -1,  1),

                                                        new Point3D(-1,  1,  1),
                                                        new Point3D( 1,  1,  1),
                                                        new Point3D(-1,  1, -1),

                                                        new Point3D( 1,  1, -1),
                                                        new Point3D( 1,  1,  1),
                                                        new Point3D(-1,  1,  1)
                                                        };

            Vector3DCollection normals = new Vector3DCollection() { new Vector3D(0, 0, 1),
                                                                    new Vector3D(0, 0, 1),
                                                                    new Vector3D(0, 0, 1),

                                                                    new Vector3D(0, 0, 1),
                                                                    new Vector3D(0, 0, 1),
                                                                    new Vector3D(0, 0, 1),

                                                                    new Vector3D(0, 0, 1),
                                                                    new Vector3D(0, 0, 1),
                                                                    new Vector3D(0, 0, 1),
 
                                                                    new Vector3D(0, 0, 1),
                                                                    new Vector3D(0, 0, 1),
                                                                    new Vector3D(0, 0, 1),
                                                                    };

            Int32Collection triangleIdx = new Int32Collection() { 3,2,1,    1,0,3,    7,6,5,  
                                                                  5,4,7,    11,10,9,  9,8,11,
                                                                  15,14,13, 13,12,15, 19,18,17,
                                                                  17,16,19, 23,22,21, 21,20,23
                                                                };

            PointCollection texcoords = new PointCollection()     { new Point(1, 1),
                                                                    new Point(0, 1),
                                                                    new Point(0, 0),

                                                                    new Point(1, 0),
                                                                    new Point(1, 1),
                                                                    new Point(0, 1),
                                                                    
                                                                    new Point(0, 0),
                                                                    new Point(1, 0),
                                                                    new Point(1, 1),

                                                                    new Point(0, 1),
                                                                    new Point(0, 0),
                                                                    new Point(1, 0),

                                                                    new Point(1, 1),
                                                                    new Point(0, 1),
                                                                    new Point(0, 0),
                                                                    
                                                                    new Point(1, 0),
                                                                    new Point(1, 1),
                                                                    new Point(0, 1),

                                                                    new Point(0, 0),
                                                                    new Point(1, 0),
                                                                    new Point(1, 1),

                                                                    new Point(0, 1),
                                                                    new Point(0, 0),
                                                                    new Point(1, 0)
                                                                    };


            mesh.Positions = pointCollection;
            mesh.Normals = normals;
            mesh.TriangleIndices = triangleIdx;
            mesh.TextureCoordinates = texcoords;

            GeometryModel3D model = new GeometryModel3D(mesh, mat);
            model.BackMaterial = mat;

            cube.Content = model;
            view.Children.Add(cube);
        }

        private void CalcNormals()
        {
            int tris = pointCollection.Count / 3;
            normals = new Vector3D[tris];
            for (int i = 0; i < tris; i += 3)
            {
                normals[i] = normal(pointCollection[i], pointCollection[i + 1], pointCollection[i + 2]);
            }
        }

        private Vector3D normal(Point3D a, Point3D b, Point3D c)
        {
            var v1 = new Vector3D(b.X - a.X, b.Y - a.Y, b.Z - a.Z);
            var v2 = new Vector3D(c.X - b.X, c.Y - b.Y, c.Z - b.Z);
            return Vector3D.CrossProduct(v1, v2);
        }

        /*
            The ray-triangle intersection point p must be along the ray (i.e. p = o + td)
            and must be in the plane of the triangle (i.e. the vector from a triangle vertex to p
            must be perpendicular to the triangle normal), (p − a) · n = 0. From these two
            equations we get:
          0 = (p − a) · n
            = (o + td − a) · n
            = (o − a) · n + td · n
          t = (a - o) · n/(d · n)
        */
        private double intersect(Vector3D origin, Vector3D end, Vector3D direction, Vector3D normal)
        {
            // Vector is parallel to the triangle and will result in (a-o) dot n / 0
            if (Vector3D.DotProduct(direction, normal) == 0)
                return 0;

            return Vector3D.DotProduct(Vector3D.Subtract(end, origin), normal) / Vector3D.DotProduct(direction, normal);
        }

        //a + u(b − a) + v(c − a) = p
        private bool barycentric(Point3D a, Point3D b, Point3D c, Point3D P)
        {
            // Compute vectors
            Vector3D v0 = new Vector3D(b.X - a.X, b.Y - a.Y, b.Z - a.Z); // B - A
            Vector3D v1 = new Vector3D(c.X - a.X, c.Y - a.Y, c.Z - a.Z); // C - A
            Vector3D v2 = new Vector3D(P.X - a.X, P.Y - a.Y, P.Z - a.Z); // P - A

            //Compute dot products
            double dot00 = Vector3D.DotProduct(v0, v0);
            double dot01 = Vector3D.DotProduct(v0, v1);
            double dot02 = Vector3D.DotProduct(v0, v2);
            double dot11 = Vector3D.DotProduct(v1, v1);
            double dot12 = Vector3D.DotProduct(v1, v2);

            // Compute barycentric coordinates
            double invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
            double u = (dot11 * dot02 - dot01 * dot12) * invDenom;
            double v = (dot00 * dot12 - dot01 * dot02) * invDenom;

            return (u > 0) && (v > 0) && (u + v < 1);
        }

        private Matrix3D GetCameraMatrix()
        {
            Matrix3D matrix = new Matrix3D();
            matrix.OffsetX = view.Camera.Transform.Value.OffsetX;
            matrix.OffsetY = view.Camera.Transform.Value.OffsetY;
            matrix.OffsetZ = view.Camera.Transform.Value.OffsetZ;
            matrix.M11 = view.Camera.Transform.Value.M11;
            matrix.M12 = view.Camera.Transform.Value.M12;
            matrix.M13 = view.Camera.Transform.Value.M13;
            matrix.M14 = view.Camera.Transform.Value.M14;
            matrix.M21 = view.Camera.Transform.Value.M21;
            matrix.M22 = view.Camera.Transform.Value.M22;
            matrix.M23 = view.Camera.Transform.Value.M23;
            matrix.M24 = view.Camera.Transform.Value.M24;
            matrix.M31 = view.Camera.Transform.Value.M31;
            matrix.M32 = view.Camera.Transform.Value.M32;
            matrix.M33 = view.Camera.Transform.Value.M33;
            matrix.M34 = view.Camera.Transform.Value.M34;
            matrix.M44 = view.Camera.Transform.Value.M44;
            return matrix;
        }
    }
}
